/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Block TEA (xxtea) Tiny Encryption Algorithm implementation in JavaScript                      */
/*     (c) Chris Veness 2002-2010: www.movable-type.co.uk/tea-block.html                          */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Algorithm: David Wheeler & Roger Needham, Cambridge University Computer Lab                   */
/*             http://www.cl.cam.ac.uk/ftp/papers/djw-rmn/djw-rmn-tea.html (1994)                 */
/*             http://www.cl.cam.ac.uk/ftp/users/djw3/xtea.ps (1997)                              */
/*             http://www.cl.cam.ac.uk/ftp/users/djw3/xxtea.ps (1998)                             */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */


/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Base64 class: Base 64 encoding / decoding (c) Chris Veness 2002-2010                          */
/*    note: depends on Utf8 class                                                                 */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

class Base64 {
    private static code: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    /**
     * Encode string into Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648]
     * (instance method extending String object). As per RFC 4648, no newlines are added.
     *
     * @param {String} str The string to be encoded as base-64
     * @param {Boolean} [utf8encode=false] Flag to indicate whether str is Unicode string to be encoded 
     *   to UTF8 before conversion to base64; otherwise string is assumed to be 8-bit characters
     * @returns {String} Base64-encoded string
     */
    public static encode(str: string, utf8encode ? : boolean) { // http://tools.ietf.org/html/rfc4648
        utf8encode = (typeof utf8encode == 'undefined') ? false : utf8encode;
        var o1, o2, o3, bits, h1, h2, h3, h4, e = [],
            pad = '',
            c, plain, coded;
        var b64 = this.code;

        plain = utf8encode ? Utf8.encode(str) : str;

        c = plain.length % 3; // pad string to length of multiple of 3
        if (c > 0) {
            while (c++ < 3) {
                pad += '=';
                plain += '\0';
            }
        }
        // note: doing padding here saves us doing special-case packing for trailing 1 or 2 chars

        for (c = 0; c < plain.length; c += 3) { // pack three octets into four hexets
            o1 = plain.charCodeAt(c);
            o2 = plain.charCodeAt(c + 1);
            o3 = plain.charCodeAt(c + 2);

            bits = o1 << 16 | o2 << 8 | o3;

            h1 = bits >> 18 & 0x3f;
            h2 = bits >> 12 & 0x3f;
            h3 = bits >> 6 & 0x3f;
            h4 = bits & 0x3f;

            // use hextets to index into code string
            e[c / 3] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
        }
        coded = e.join(''); // join() is far faster than repeated string concatenation in IE

        // replace 'A's from padded nulls with '='s
        coded = coded.slice(0, coded.length - pad.length) + pad;

        return coded;
    }

    /**
     * Decode string from Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648]
     * (instance method extending String object). As per RFC 4648, newlines are not catered for.
     *
     * @param {String} str The string to be decoded from base-64
     * @param {Boolean} [utf8decode=false] Flag to indicate whether str is Unicode string to be decoded 
     *   from UTF8 after conversion from base64
     * @returns {String} decoded string
     */
    public static decode(str: string, utf8decode ? : boolean) {
        utf8decode = (typeof utf8decode == 'undefined') ? false : utf8decode;
        var o1, o2, o3, h1, h2, h3, h4, bits, d = [],
            plain, coded;
        var b64 = this.code;

        coded = utf8decode ? Utf8.decode(str) : str;


        for (var c = 0; c < coded.length; c += 4) { // unpack four hexets into three octets
            h1 = b64.indexOf(coded.charAt(c));
            h2 = b64.indexOf(coded.charAt(c + 1));
            h3 = b64.indexOf(coded.charAt(c + 2));
            h4 = b64.indexOf(coded.charAt(c + 3));

            bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;

            o1 = bits >>> 16 & 0xff;
            o2 = bits >>> 8 & 0xff;
            o3 = bits & 0xff;

            d[c / 4] = String.fromCharCode(o1, o2, o3);
            // check for padding
            if (h4 == 0x40) d[c / 4] = String.fromCharCode(o1, o2);
            if (h3 == 0x40) d[c / 4] = String.fromCharCode(o1);
        }
        plain = d.join(''); // join() is far faster than repeated string concatenation in IE

        return utf8decode ? Utf8.decode(plain) : plain;
    }
}; // Base64 namespace



/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Utf8 class: encode / decode between multi-byte Unicode characters and UTF-8 multiple          */
/*              single-byte character encoding (c) Chris Veness 2002-2010                         */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

class Utf8 {
    /**
     * Encode multi-byte Unicode string into utf-8 multiple single-byte characters 
     * (BMP / basic multilingual plane only)
     *
     * Chars in range U+0080 - U+07FF are encoded in 2 chars, U+0800 - U+FFFF in 3 chars
     *
     * @param {String} strUni Unicode string to be encoded as UTF-8
     * @returns {String} encoded string
     */
    public static encode(strUni: string) {
        // use regular expressions & String.replace callback function for better efficiency 
        // than procedural approaches
        var strUtf = strUni.replace(
            /[\u0080-\u07ff]/g, // U+0080 - U+07FF => 2 bytes 110yyyyy, 10zzzzzz
            function (c) {
                var cc = c.charCodeAt(0);
                return String.fromCharCode(0xc0 | cc >> 6, 0x80 | cc & 0x3f);
            }
        );
        strUtf = strUtf.replace(
            /[\u0800-\uffff]/g, // U+0800 - U+FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz
            function (c) {
                var cc = c.charCodeAt(0);
                return String.fromCharCode(0xe0 | cc >> 12, 0x80 | cc >> 6 & 0x3F, 0x80 | cc & 0x3f);
            }
        );
        return strUtf;
    }

    /**
     * Decode utf-8 encoded string back into multi-byte Unicode characters
     *
     * @param {String} strUtf UTF-8 string to be decoded back to Unicode
     * @returns {String} decoded string
     */
    public static decode(strUtf: string) {
        // note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
        var strUni = strUtf.replace(
            /[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars
            function (c) { // (note parentheses for precence)
                var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f);
                return String.fromCharCode(cc);
            }
        );
        strUni = strUni.replace(
            /[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars
            function (c) { // (note parentheses for precence)
                var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f;
                return String.fromCharCode(cc);
            }
        );
        return strUni;
    }

}; // Utf8 namespace

export default class TEA {
    /*
     * encrypt text using Corrected Block TEA (xxtea) algorithm
     *
     * @param {string} plaintext String to be encrypted (multi-byte safe)
     * @param {string} password  Password to be used for encryption (1st 16 chars)
     * @returns {string} encrypted text
     */
    public encrypt(plaintext: string, password: string) {
        if (plaintext.length == 0) return (''); // nothing to encrypt

        // convert string to array of longs after converting any multi-byte chars to UTF-8
        var v = this.strToLongs(Utf8.encode(plaintext));
        if (v.length <= 1) v[1] = 0; // algorithm doesn't work for n<2 so fudge by adding a null
        // simply convert first 16 chars of password as key
        var k = this.strToLongs(Utf8.encode(password).slice(0, 16));
        var n = v.length;

        // ---- <TEA coding> ---- 

        var z = v[n - 1],
            y = v[0],
            delta = 0x9E3779B9;
        var mx, e, q = Math.floor(6 + 52 / n),
            sum = 0;

        while (q-- > 0) { // 6 + 52/n operations gives between 6 & 32 mixes on each word
            sum += delta;
            e = sum >>> 2 & 3;
            for (var p = 0; p < n; p++) {
                y = v[(p + 1) % n];
                mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
                z = v[p] += mx;
            }
        }

        // ---- </TEA> ----

        var ciphertext = this.longsToStr(v);

        return Base64.encode(ciphertext);
    }

    /*
     * decrypt text using Corrected Block TEA (xxtea) algorithm
     *
     * @param {string} ciphertext String to be decrypted
     * @param {string} password   Password to be used for decryption (1st 16 chars)
     * @returns {string} decrypted text
     */
    public decrypt(ciphertext: string, password: string) {
        if (ciphertext.length == 0) return ('');
        var v = this.strToLongs(Base64.decode(ciphertext));
        var k = this.strToLongs(Utf8.encode(password).slice(0, 16));
        var n = v.length;

        // ---- <TEA decoding> ---- 

        var z = v[n - 1],
            y = v[0],
            delta = 0x9E3779B9;
        var mx, e, q = Math.floor(6 + 52 / n),
            sum = q * delta;

        while (sum != 0) {
            e = sum >>> 2 & 3;
            for (var p = n - 1; p >= 0; p--) {
                z = v[p > 0 ? p - 1 : n - 1];
                mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
                y = v[p] -= mx;
            }
            sum -= delta;
        }

        // ---- </TEA> ---- 

        var plaintext = this.longsToStr(v);

        // strip trailing null chars resulting from filling 4-char blocks:
        plaintext = plaintext.replace(/\0+$/, '');

        return Utf8.decode(plaintext);
    }

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

    // supporting functions

    private strToLongs(s: string): number[] { // convert string to array of longs, each containing 4 chars
        // note chars must be within ISO-8859-1 (with Unicode code-point < 256) to fit 4/long
        let l = new Array(Math.ceil(s.length / 4));
        for (var i = 0; i < l.length; i++) {
            // note little-endian encoding - endianness is irrelevant as long as 
            // it is the same in longsToStr() 
            l[i] = s.charCodeAt(i * 4) + (s.charCodeAt(i * 4 + 1) << 8) +
                (s.charCodeAt(i * 4 + 2) << 16) + (s.charCodeAt(i * 4 + 3) << 24);
        }
        return l; // note running off the end of the string generates nulls since 
    } // bitwise operators treat NaN as 0

    private longsToStr(l: number[]): string { // convert array of longs back to string
        var a = new Array(l.length);
        for (var i = 0; i < l.length; i++) {
            a[i] = String.fromCharCode(l[i] & 0xFF, l[i] >>> 8 & 0xFF,
                l[i] >>> 16 & 0xFF, l[i] >>> 24 & 0xFF);
        }
        return a.join(''); // use Array.join() rather than repeated string appends for efficiency in IE
    }
}



/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */